package kuberneteshelper

import (
	"context"
	"fmt"
	"reflect"
	"sort"
	"strings"
	"time"

	"v8s-controller/internal"
	"v8s-controller/internal/k8s"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/serializer/json"
	"k8s.io/client-go/kubernetes"
)

type (
	ResourceCollection struct {
		resources []runtime.Object
	}
)

var (
	systemNamespaces []string
	serializer       *json.Serializer
)

func init() {
	serializer = json.NewSerializerWithOptions(json.DefaultMetaFactory, nil, nil, json.SerializerOptions{
		Yaml:   true,
		Strict: true,
	})

	systemNamespaces = []string{
		"default",
		"kube-system",
		"kube-public",
		"kube-node-lease",
	}
}

func collectResources(ctx context.Context, client *kubernetes.Clientset) *ResourceCollection {
	resources := make([]runtime.Object, 0)

	namespaces := getUserNamespaces(ctx, client)

	for _, item := range namespaces {
		current := item
		resources = append(resources, current)

		supported, err := k8s.ListAllSupportedResources(ctx, client, internal.Namespace(item.Name))
		if err != nil {
			panic(fmt.Errorf("failed to list supported resources: %w", err))
		}

		for _, resource := range supported {
			if isIgnored(resource) {
				continue
			}

			resources = append(resources, resource.Resource())
		}
	}

	return &ResourceCollection{resources: resources}
}

func getUserNamespaces(ctx context.Context, client *kubernetes.Clientset) []*corev1.Namespace {
	namespaces, err := k8s.ListNamespaces(ctx, client)
	if err != nil {
		panic(fmt.Errorf("cannot obtain namespaces: %w", err))
	}

	result := make([]*corev1.Namespace, 0, len(namespaces)-len(systemNamespaces))

	for _, namespace := range namespaces {
		current := namespace
		if isSystemNamespace(namespace) {
			continue
		}

		if current.DeletionTimestamp != nil {
			continue
		}

		result = append(result, current)
	}

	return result
}

func isSystemNamespace(namespace *corev1.Namespace) bool {
	for _, system := range systemNamespaces {
		if system == namespace.Name {
			return true
		}
	}

	return false
}

func isIgnored(resource *k8s.DescribableResource) bool {
	switch resource.Kind() {
	case k8s.SecretKind:
		if strings.HasPrefix(resource.Name(), "default-token-") {
			return true
		}

		if resource.Name() == k8s.IndexedSecretName {
			return true
		}
	}

	return false
}

func (c *ResourceCollection) String() string {
	builder := &strings.Builder{}

	resources := c.resources

	sort.SliceStable(resources, func(i, j int) bool {
		left := resources[i]
		right := resources[j]

		leftResource, err := k8s.ToDescribableResource(left)
		if err != nil {
			panic(leftResource)
		}

		rightResource, err := k8s.ToDescribableResource(right)
		if err != nil {
			panic(leftResource)
		}

		if leftResource.Namespace() != rightResource.Namespace() {
			return strings.Compare(leftResource.Namespace(), rightResource.Namespace()) < 0
		}

		if leftResource.Kind() != rightResource.Kind() {
			return strings.Compare(string(leftResource.Kind()), string(rightResource.Kind())) < 0
		}

		return strings.Compare(leftResource.Name(), rightResource.Name()) < 0
	})

	for _, resource := range resources {
		clearDynamicProperties(resource)

		builder.WriteString("---\n")

		if err := serializer.Encode(resource, builder); err != nil {
			panic(fmt.Errorf("failed to encode resource: %w", err))
		}
	}

	return builder.String()
}

func clearDynamicProperties(object runtime.Object) {
	objectMeta, err := meta.Accessor(object)
	if err != nil {
		panic(fmt.Errorf("cannot access meta on kind '%s': %w", object.GetObjectKind(), err))
	}

	objectMeta.SetManagedFields(nil)
	objectMeta.SetCreationTimestamp(metav1.NewTime(time.Date(2000, 10, 1, 0, 0, 0, 0, time.UTC)))
	objectMeta.SetUID("mocked-uid")
	objectMeta.SetResourceVersion("mocked-version")

	delete(objectMeta.GetAnnotations(), "deployment.kubernetes.io/revision")

	clear("Status", object)
}

func clear(field string, object interface{}) {
	reflection := reflect.ValueOf(object)

	if reflection.Kind() == reflect.Ptr {
		reflection = reflection.Elem()
	}

	if reflection.Kind() != reflect.Struct {
		panic("not a struct")
	}

	property := reflection.FieldByName(field)

	if !property.IsValid() {
		// field is not present on object
		return
	}

	if !property.CanSet() {
		fmt.Printf("cannot clear field '%s'\n", field)
		panic(fmt.Sprintf("cannot clear field '%s'", field))
	}

	property.Set(reflect.Zero(property.Type()))
}
